package com.log.enhance.aspect;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SimplePropertyPreFilter;
import com.alibaba.fastjson.support.spring.PropertyPreFilters;
import com.log.enhance.annotation.LogInfo;
import com.log.enhance.config.LogContext;
import com.log.enhance.constant.LogConst;
import com.log.enhance.constant.LogPluginConstant;
import com.log.enhance.core.service.FilterResultService;
import com.log.enhance.core.service.LogUserInfoService;
import com.log.enhance.util.SPELUtil;
import lombok.SneakyThrows;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.slf4j.ext.XLoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.Proxy;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 日志切面
 * 实现ApplicationContextAware接口，方便在容器启动的时候检查容器中是否注入了获取用户信息的对象（FilterResultService）
 * 如果有这样的bean，则在日志中打印用户信息
 *
 * @author Mr_wenpan@163.com 2021/5/29 11:51 上午
 */
@Aspect
@Component
public class LogAspect implements ApplicationContextAware {

    private static final String USER = "user";

    private static final String DOT_NOTATION = ".";

    private static final Logger LOG = XLoggerFactory.getXLogger(LogAspect.class);

    @Autowired
    private List<FilterResultService<?, ?>> filterResultServices;

    /**
     * 用户信息
     */
    LogUserInfoService logUserInfoService;

    @Pointcut("@annotation(com.log.enhance.annotation.LogInfo)")
    public void log() {
    }

    /**
     * 环绕通知,日志执行不能影响目标方法
     *
     * @param joinPoint 连接点
     * @return java.lang.Object
     * @author Mr_wenpan@163.com 2021/5/29 11:51 上午
     */
    @SneakyThrows
    @Around(value = "log()")
    public Object arround(ProceedingJoinPoint joinPoint) {
        Object result = null;

        // 获取日志上下文（里面包含了日志内容）
        LogThreadContext ltc = LogThreadContext.getLogThreadContext();

        // 获取被增强的方法的相关信息
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();

        // 这里日志动作不能影响目标方法的执行，所以需要自己把异常catch掉
        try {
            try {
                // 初始化日志内容
                initLogContext(method, ltc);
                // 执行目标方法前
                handBeforeMethodLog(joinPoint, ltc);
            } catch (Exception e) {
                LOG.warn("handlerLogMethod before : ", e);
            }
            //方法执行，并获取返回值，目标方法发生异常时，使用@SneakyThrows向上抛出
            result = joinPoint.proceed();
        } finally {
            try {
                // 执行目标方法后
                handAfterMethodLog(joinPoint, ltc, result);
            } catch (Exception e) {
                LOG.warn("handlerLogMethod finally : ", e);
            }
        }

        // 返回目标方法执行的返回值
        return result;
    }

    /**
     * 初始化日志上下文
     *
     * @param method 方法对象
     * @param ltc    日志线程上下文
     * @author Mr_wenpan@163.com 2021/5/29 12:36 下午
     */
    private void initLogContext(Method method, LogThreadContext ltc) {
        // 获取方法上的注解，并将注解上用户设置的配置，设置到LogContext中
        LogInfo logAnnotation = method.getAnnotation(LogInfo.class);
        LogContext logContext = new LogContext();
        logContext.setEnableLog(true);
        logContext.setAction(logAnnotation.action());
        logContext.setItemType(logAnnotation.itemType());
        logContext.setItemIds(logAnnotation.itemIds());
        logContext.setPrintDebugLog(logAnnotation.printInfoLog());
        logContext.setPrintDebugLog(logAnnotation.printInfoLog());
        logContext.setExcludeInParam(logAnnotation.excludeInParam());
        logContext.setIncludeInParam(logAnnotation.includeInParam());
        logContext.setParam(logAnnotation.param());
        // 保存到当前上下文中
        ltc.setCurrentLogContext(logContext);
    }

    /**
     * 目标方法调用前的日志操作
     *
     * @param joinPoint 连接点
     * @param ltc       日志线程上下文
     * @return org.slf4j.Logger
     * @author Mr_wenpan@163.com 2021/5/29 12:44 下午
     */
    private Logger handBeforeMethodLog(ProceedingJoinPoint joinPoint, LogThreadContext ltc) {

        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        LogContext currentLogContext = ltc.getCurrentLogContext();
        boolean printInfoLog = currentLogContext.isPrintDebugLog();
        // String[] params = currentLogContext.getParam();
        Object target = joinPoint.getTarget();
        Class<?> targetClass = target.getClass();
        Logger logger = LoggerFactory.getLogger(targetClass);

        // 如果开启了打印debug日志或现在处于debug模式，则打印日志
        if (logger.isDebugEnabled() || printInfoLog) {
            // 构建日志信息
            currentLogContext.setPrintDebugLog(true);

            //===============================================================================
            //  保存action、itemType、itemId到MDC(这里暂时只实现了保存用户信息到MDC中)
            // 这里可以考虑放到添加traceId的过滤器中
            //===============================================================================
            if (logUserInfoService != null) {
                saveLogInfo2MDC(joinPoint, ltc);
            }

            StringBuilder mesInfo = new StringBuilder();
            // 如果是代理对象
            if (target instanceof Proxy) {
                // 获取被代理对象的全类名
                mesInfo.append(signature.getDeclaringTypeName()).append(".");
            }
            // 获取方法名
            mesInfo.append(signature.getName());
            // 组装提示信息
            mesInfo.append(" start parameters: 【 {} 】");
            // 所在的类.方法
            String methodStr = getMethodParam(joinPoint, currentLogContext);

            // 打印方法入参
            if (printInfoLog) {
                logger.info(mesInfo.toString(), methodStr);
            } else {
                logger.debug(mesInfo.toString(), methodStr);
            }

        } else {
            currentLogContext.setPrintDebugLog(false);
        }

        return logger;

    }

    /**
     * 目标方法调用后的日志操作
     *
     * @param joinPoint 连接点
     * @param ltc       日志线程上下文
     * @param result    目标方法返回结果
     * @author Mr_wenpan@163.com 2021/5/29 12:43 下午
     */
    private void handAfterMethodLog(ProceedingJoinPoint joinPoint, LogThreadContext ltc, Object result) {
        LogContext logContext = ltc.getCurrentLogContext();
        // 是否需要打印日志
        boolean printLog = logContext.isPrintDebugLog();
        // 如果是数组 是否打印出参大小，不打印对象值
        boolean printOutParamSize = logContext.isPrintOutParamSize();
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        StringBuilder endString = new StringBuilder();

        // 获取被代理的对象
        Object target = joinPoint.getTarget();

        Logger logger = LoggerFactory.getLogger(target.getClass());
        if (target instanceof Proxy) {
            // signature.getDeclaringTypeName()获取全类名
            endString.append(signature.getDeclaringTypeName()).append(".").append(signature.getName());
        }
        endString.append(signature.getName());
        endString.append(" end ");

        // 只有debug模式或用户动态修改强制在info级别下打印debug模式下的参数
        if (logger.isDebugEnabled() || printLog) {
            // 如果该方法的返回值是集合类型并且开启了打印集合大小属性，则打印集合大小
            if (result instanceof Collection && printOutParamSize) {
                // 打印出参集合大小
                logger.info(endString.append(" output parameters size: {}").toString(), ((Collection<?>) result).size());
                return;
            }

            //
            // 处理返回值是对象里面包着集合的情况（比如controller的返回值）
            // -------------------------------------------------------
            String responseStr = null;
            try {
                if (CollectionUtils.isNotEmpty(filterResultServices)) {
                    for (FilterResultService filterResultService : filterResultServices) {
                        Optional<?> filterResult = filterResultService.filterResult(result);
                        // 返回null，说明不需要处理  返回Optional.empty(),说明处理了，处理结果为null
                        if (filterResult.isPresent()) {
                            LOG.debug("返回结果处理成功");
                            Object fr = filterResult.get();
                            if (fr instanceof Collection && printOutParamSize) {
                                logger.info(endString.append(" output parameters size: {}").toString(),
                                        ((Collection<?>) fr).size());
                                return;
                            } else {
                                responseStr = JSON.toJSONString(filterResult);
                            }
                            break;
                        }
                    }
                }
            } catch (Exception e) {
                LOG.warn("handAfterMethodLog error : ", e);
            }

            if (StringUtils.isEmpty(responseStr)) {
                responseStr = result == null ? "void" : JSON.toJSONString(result);
            }
            endString.append("output parameters: ").append(responseStr);
            // 只有在用户开启了打印日志，获取debug模式下才打印出参数据
            if (printLog) {
                logger.info(endString.toString());
            } else {
                logger.debug(endString.toString());
            }

        } else {
            // 否则值打印xxx方法执行完毕
            logger.info(endString.toString());
        }

    }

    /**
     * 保存日志信息到MDC中
     *
     * @param pjp 连接点
     * @param ltc 日志线程上下文
     * @author Mr_wenpan@163.com 2021/5/29 9:22 下午
     */
    private void saveLogInfo2MDC(ProceedingJoinPoint pjp, LogThreadContext ltc) {
        // 保存用户信息到MDC，logUserInfoService接口依赖引入我们日志插件的应用服务实现，然后注入到容器即可
        if (logUserInfoService != null) {
            String userInfo = logUserInfoService.getUserInfo();
            if (StringUtils.isNotEmpty(userInfo)) {
                MDC.put(USER, userInfo);
            }
        }
        JSONObject json = getLogJson(pjp, ltc);
        if (!json.isEmpty()) {
            String jsonString = json.toJSONString();
            MDC.put(LogPluginConstant.LOG_JSON, jsonString);
        }
    }

    /**
     * 获取注解Log的值,构造成json，供sf4j使用
     *
     * @return com.alibaba.fastjson.JSONObject
     */
    private JSONObject getLogJson(ProceedingJoinPoint pjp, LogThreadContext ltc) {
        LogContext logContext = ltc.getCurrentLogContext();
        LogConst.Action action = logContext.getAction();
        String itemType = logContext.getItemType();
        String[] itemIds = logContext.getItemIds();
        SPELUtil spel = new SPELUtil(pjp);
        JSONObject json = new JSONObject();
        // 操作
        if (!LogConst.Action.NULL.equals(action)) {
            json.put("A", action.toString());
        }
        // 对象类型
        if (StringUtils.isNotEmpty(itemType)) {
            json.put("T", itemType);
        }
        // 对象类型
        if (itemIds.length > 0) {
            for (String itemId : itemIds) {
                if (itemId.contains(DOT_NOTATION)) {
                    String substring = itemId.substring(itemId.indexOf(DOT_NOTATION) + 1);
                    json.put(substring, spel.cacl(itemId));
                } else {
                    json.put(itemId, spel.cacl(itemId));
                }
            }
        }
        return json;
    }

    /**
     * 获取方法入参
     *
     * @return java.lang.String
     * @author Mr_wenpan@163.com
     */
    private String getMethodParam(ProceedingJoinPoint point, LogContext logContext) {
        // 获取每个参数值
        Object[] methodArgs = point.getArgs();
        // 获取参数名
        Parameter[] parameters = ((MethodSignature) point.getSignature()).getMethod().getParameters();
        String requestStr;
        try {
            requestStr = logParam(parameters, methodArgs, logContext);
        } catch (Exception e) {
            LOG.warn("failed to get parameters : ", e);
            requestStr = "failed to get parameters";
        }
        return requestStr;
    }

    /**
     * 拼接入参
     *
     * @param paramsArgsName  参数名称
     * @param paramsArgsValue 参数的值
     * @param logContext      日志上下文
     * @return java.lang.String
     * @author Mr_wenpan@163.com
     */
    private String logParam(Parameter[] paramsArgsName, Object[] paramsArgsValue, LogContext logContext) {
        if (ArrayUtils.isEmpty(paramsArgsName) || ArrayUtils.isEmpty(paramsArgsValue)) {
            return "";
        }
        String[] excludeInParam = logContext.getExcludeInParam();
        String[] includeInParam = logContext.getIncludeInParam();
        Map<String, List<String>> excludeCollect = null;
        Map<String, List<String>> includeCollect = null;
        if (ArrayUtils.isNotEmpty(includeInParam)) {
            includeCollect = Arrays.stream(includeInParam)
                    .filter(a -> a.startsWith("arg") && a.contains("."))
                    .collect(Collectors.groupingBy(e -> e.substring(0, 4)));
        }
        if (ArrayUtils.isNotEmpty(excludeInParam)) {
            excludeCollect = Arrays.stream(excludeInParam)
                    .filter(a -> a.startsWith("arg") && a.contains("."))
                    .collect(Collectors.groupingBy(e -> e.substring(0, 4)));
        }
        StringBuilder buffer = new StringBuilder();
        // 遍历方法的每一个参数
        Flag:
        for (int i = 0; i < paramsArgsValue.length; i++) {
            //参数名
            String name = paramsArgsName[i].getName();
            //参数值
            Object value = paramsArgsValue[i];
            //
            // 判断当前参数值是否属于excludeInParam，如果属于，则跳过不进行拼接
            // ------------------------------------------------------------------------------
            for (String exclude : excludeInParam) {
                if (name.equals(exclude)) {
                    continue Flag;
                }
                Class<?> aClass = value.getClass();
                // 如果该参数的名称或全类名等于要排除的参数
                if (exclude.equals(aClass.getSimpleName()) || exclude.equals(aClass.getName())) {
                    continue Flag;
                }
            }
            buffer.append(name).append("=");

            // 字符串类型参数
            if (value instanceof String) {
                buffer.append(value).append(" , ");
            } else {
                // 非字符串类型参数处理
                PropertyPreFilters filter = new PropertyPreFilters();
                if (null != includeCollect) {
                    List<String> list = includeCollect.get(name);
                    if (null != list) {
                        for (String e : list) {
                            filter.addFilter(e.substring(e.indexOf(".") + 1));
                        }
                    }
                }

                if (null != excludeCollect) {
                    List<String> list = excludeCollect.get(name);
                    if (null != list) {
                        for (String e : list) {
                            filter.addFilter().addExcludes(e.substring(e.indexOf(".") + 1));
                        }
                    }
                }

                List<PropertyPreFilters.MySimplePropertyPreFilter> filters = filter.getFilters();

                if (null != filters && filters.size() > 0) {
                    SimplePropertyPreFilter[] simplePropertyPreFilters = new SimplePropertyPreFilter[filters.size()];
                    for (int i1 = 0; i1 < filters.size(); i1++) {
                        simplePropertyPreFilters[i1] = filters.get(i1);
                    }
                    buffer.append(JSON.toJSONString(value, simplePropertyPreFilters)).append(",");
                } else {
                    buffer.append(JSON.toJSONString(value)).append(",");
                }

            }
        }
        return buffer.toString();
    }

    @Override
    public void setApplicationContext(ApplicationContext context) {
        try {
            // 去容器中通过类型获取一下，有没有LogUserInfoService类型的bean注入，如果有则每条日志信息带上操作用户
            logUserInfoService = context.getBean(LogUserInfoService.class);
        } catch (BeansException e) {
            LOG.info("logService is null");
        }
    }

}